Skip to content

Add Thrift-native metadata support - #1641

Merged
vuanhphung merged 5 commits into
mainfrom
vu-phung_data/productionize-thrift-native-metadata
Aug 26, 2026
Merged

Add Thrift-native metadata support#1641
vuanhphung merged 5 commits into
mainfrom
vu-phung_data/productionize-thrift-native-metadata

Conversation

@vuanhphung

@vuanhphung vuanhphung commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds opt-in EnableThriftNativeMetadata support for metadata operations executed through SEA. The server can return Thrift-shaped metadata rows through the Statement Execution API, while the driver preserves the same filtering, normalization, error behavior, and JDBC metadata exposed by the Thrift client.

Supported operations are catalogs, schemas, tables, columns, functions, primary keys, and cross references. Procedures and procedure columns continue to use the existing SEA path.

Request and result flow

DatabaseMetaData request
  → send X-Databricks-Metadata-Operation-Type
  → when enabled and supported, request Thrift-native metadata
  → inspect ResultManifest.is_native_metadata_result
      false / absent → existing SEA SHOW-result processing
      true           → copy native rows and rebuild them with the existing
                       Thrift normalization and JDBC metadata builders

The manifest flag is authoritative: neither the request header nor a native-looking schema changes how a result is processed. This keeps the feature opt-in and preserves legacy behavior when the server does not return a native result.

Requests requiring special handling

  • getTables: the native path sends catalog and types to runtime, then reapplies the JDBC filters to returned rows. This is necessary because runtime treats catalog as a pattern, may return temporary views outside the requested catalog, and does not consistently handle empty or exact table-type filters. SEA native results reuse this processing to preserve existing JDBC-over-Thrift behavior.

    Path Catalog behavior types behavior
    Native GetTables Applies the direct-Thrift exact filter using the original JDBC catalog null accepts every runtime-returned type, an empty array returns no rows, and a non-empty array is matched exactly
    SHOW TABLES SQL is scoped to the resolved catalog null uses the driver's supported default types because SHOW has no JDBC types argument
  • getFunctions: the runtime paths have different catalog semantics:

    Path Search behavior Runtime FUNCTION_CAT Driver handling
    Native GetFunctions Ignores the requested catalog and searches the session catalog Always "" Replaces it with the original DatabaseMetaData.getFunctions catalog, including null
    SHOW FUNCTIONS Searches the requested/resolved catalog Returns the function identifier's catalog Leaves it unchanged

    The driver therefore saves the original JDBC catalog before resolving a catalog for SQL construction and passes that original value only when rebuilding a manifest-confirmed native result. This preserves Thrift compatibility, but it is a column-label correction rather than catalog filtering: native rows can come from catalog A and be labeled as catalog B.

  • getCrossReference: the SQL query narrows only the foreign-key side, so the returned native rows are additionally filtered by the requested parent catalog, schema, and table.

  • Native metadata failures use Thrift-compatible propagation and timeout codes instead of the legacy SHOW-query compatibility fallbacks.

SQLState correction

Key-based metadata validation now reports the applicable SQLState (42000 or 08000) and keeps EXECUTE_STATEMENT_FAILED (1003) as the driver error code. Previously these errors were constructed with DatabricksDriverErrorCode.INVALID_STATE, which incorrectly exposed INVALID_STATE through SQLException.getSQLState().

Tests: mvn spotless:check; focused core metadata suites (331 tests).

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — solid, well-tested opt-in feature with strong unit coverage across the native/legacy split, error-code/SQLState alignment, and JSON round-trip. One low-severity note about asymmetric native error propagation (listTables/listColumns propagate errors under native mode, but listFunctions/key-based ops still swallow object-not-found) — worth confirming it's intentional.

@vuanhphung
vuanhphung force-pushed the vu-phung_data/productionize-thrift-native-metadata branch from d535038 to 76205fa Compare August 14, 2026 21:35

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested opt-in feature; native metadata routing delegates cleanly to existing row builders and the SQLState/errorCode alignment looks correct. One medium concern: the fallback-bypass guard keys off the connection flag rather than the actual result, so enabling EnableThriftNativeMetadata changes getColumns/getTables error semantics even for legacy (non-native) responses. One low note on a tautological test.

@vuanhphung
vuanhphung force-pushed the vu-phung_data/productionize-thrift-native-metadata branch from 76205fa to 0a99a83 Compare August 14, 2026 22:04

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 2 Low

Solid, well-tested feature addition — opt-in Thrift-native SEA metadata with the manifest flag as the authoritative switch, good coverage across builders and header wiring. Main concerns are around the enabled-flag error-handling path: enabling the flag disables old-DBR SHOW fallbacks for the whole connection (F1), an inconsistent SQLState between null vs empty table (F2), and the native-propagation guard not applied uniformly to key-based ops (F3). All are behavior/consistency questions rather than clear defects.

@vuanhphung
vuanhphung force-pushed the vu-phung_data/productionize-thrift-native-metadata branch 3 times, most recently from 0750722 to 6eac609 Compare August 14, 2026 22:39

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Solid, well-structured feature with strong test coverage (manifest-flag gating, native vs SHOW paths, SQLState/error-code corrections, and the new post-filtering are all exercised). One low-severity robustness observation about the implicit positional-column contract between native results and the builders; otherwise the correctness, error-propagation, and JSON round-trip changes look correct.

@vuanhphung
vuanhphung force-pushed the vu-phung_data/productionize-thrift-native-metadata branch from 6eac609 to e22b219 Compare August 14, 2026 22:53
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
@vuanhphung
vuanhphung force-pushed the vu-phung_data/productionize-thrift-native-metadata branch from e22b219 to 9273799 Compare August 14, 2026 22:54

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium

Solid, well-tested opt-in feature. The manifest-flag-authoritative design is clean and the new unit coverage (native-vs-thrift equivalence, filter preservation, SQLState corrections, header behavior) is thorough. One medium note about inconsistent error-fallback behavior between metadata operations when the flag is enabled.

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested feature addition — the manifest-authoritative native-metadata path, SQLState corrections, and Thrift error-code preservation all carry matching unit coverage. One medium concern: the native error-propagation guard keys off the feature flag rather than the actual result, which can disable the legacy object-not-found/parse-error fallbacks (and make getTables/getColumns throw instead of returning empty) even for non-native SEA results; plus a low note on that policy being applied inconsistently vs. getFunctions.

getResultSet(SQL, session, MetadataOperationType.GET_TABLES), validatedTableTypes);
resultSet, requestedCatalog, resultTableTypes);
} catch (SQLException e) {
if (isThriftNativeMetadataRequested()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The error-propagation guard keys off isThriftNativeMetadataRequested() (i.e. whether the feature flag is enabled) rather than whether the result was actually native. Because the exception is thrown by getResultSet(...) before any manifest is available, there is no way at this point to know if the server actually returned a native result. The PR design states the manifest flag is authoritative and legacy behavior is preserved when the server does not return a native result — but here, once EnableThriftNativeMetadata=1, both listTables (L176) and listColumns (L227) short-circuit all the legacy fallbacks, including isObjectNotFoundException(e).

Concrete consequence: with the flag enabled against a server that does not support native metadata (older DBR that ignores the header and runs the SHOW query, or a genuinely non-existent catalog/object), getTables/getColumns now throw instead of returning an empty ResultSet. Returning empty for object-not-found is the pre-existing JDBC-contract behavior for these calls. Enabling this opt-in flag therefore silently regresses the object-not-found and SHOW ... IN ALL CATALOGS parse-error fallbacks even when the server produced a legacy SEA result. Worth confirming this is the intended tradeoff (and documenting it), or gating the propagation on something closer to "native result was actually returned."

getResultSet(SQL, session, MetadataOperationType.GET_FUNCTIONS);
return metadataResultSetBuilder.getFunctionsResult(
getResultSet(SQL, session, MetadataOperationType.GET_FUNCTIONS), catalog);
resultSet, resultSet.isThriftNativeMetadataResult() ? requestedCatalog : catalog);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The native error-handling policy is applied inconsistently across the supported operations. listTables (L176) and listColumns (L227) rethrow immediately when isThriftNativeMetadataRequested(), bypassing their isObjectNotFoundException fallback, but listFunctions (here) keeps its isObjectNotFoundException(e) → empty-result fallback with no equivalent native guard. If the intent is Thrift-compatible propagation for native metadata, functions diverges from tables/columns; if returning empty on object-not-found is the desired native behavior, then tables/columns diverge instead. Please confirm the intended policy is uniform, or add a comment explaining why functions is treated differently.

(Anchored to the nearest changed line — see the description for the exact location.)

@sreekanth-db sreekanth-db left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTables() and getColumns() use SQL fallbacks for unsupported SHOW ... IN ALL CATALOGS syntax and return empty results for object-not-found or empty-pattern cases.

Can we test the new flow for these cases against older-version compute that does not support the native metadata header?

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks solid overall — the opt-in EnableThriftNativeMetadata path is well-tested (manifest-flag authority, native filtering for tables/functions/cross-refs, SQLState/error-code corrections) and the catalog.equals(row.get(0)) flip correctly hardens against null catalog cells. One low-confidence note: the native error-propagation guard added to listTables/listColumns is not mirrored in listImportedKeys/listCrossReferences/listFunctions/listPrimaryKeys, which still fall back to empty results — likely benign for object-not-found but inconsistent with the PR's stated propagation design. Note also that the error-code change (INVALID_STATE → EXECUTE_STATEMENT_FAILED) reuses an existing code and has test coverage, so no new taxonomy entry appears required.

LOGGER.debug("listExportedKeys: table is null, throwing");
throw new DatabricksSQLException(
"Invalid argument: tableName may not be null", DatabricksDriverErrorCode.INVALID_STATE);
"Invalid argument: tableName may not be null",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — Native error-propagation guard is applied inconsistently across metadata operations. listTables (L176) and listColumns (L227) added if (isThriftNativeMetadataRequested()) throw e; before their legacy SHOW-fallbacks, so native failures propagate as the PR describes. But listImportedKeys (this line) and listCrossReferences (L493) — as well as listFunctions and listPrimaryKeys — retain their catch blocks that swallow PARSE_SYNTAX_ERROR_SQL_STATE/object-not-found and return an empty result set, with no native guard.

For object-not-found this matches Thrift's empty-result behavior, so it is likely benign. But a genuine native-path failure surfacing as 42601 (or an object-not-found classification) on these operations will still be silently converted to an empty result set rather than propagated, which contradicts the stated design and diverges from the tables/columns paths. Worth confirming this asymmetry is intentional.

(Anchored to the nearest changed line — see the description for the exact location.)

Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — a clean, well-tested opt-in feature; only 1 low-severity dead-code note. Verified the native-path row copies are mutable (so downstream row.set(...) is safe), that immutable List.of(...) from listCatalogs flows only into the copy-based getThriftRows, and that the added null checks / catalog.equals(row.get(0)) flip are genuine improvements. Nit (summary-only): the key-based validation deliberately maps null-table → 42000 but empty-table/other → 08000; the asymmetry is surprising but is intentional Thrift-compat behavior and is covered by tests, so no change requested.

&& !queryExecutionClient.getConnectionContext().getEnableMultipleCatalogSupport();
}

private boolean isThriftNativeMetadataRequested() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — isThriftNativeMetadataRequested() is introduced but never called anywhere in the module. The native-vs-SEA decision is actually driven off resultSet.isThriftNativeMetadataResult() (manifest flag) inside listTables/listFunctions, and the request-header opt-in is decided in DatabricksSdkClient.executeStatement. This helper duplicates the connection-context check that already lives in DatabricksSdkClient and is dead code. Consider removing it to avoid the impression that the query client gates native requests (it does not — the manifest flag is authoritative).

Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good overall — a well-structured, opt-in feature with strong unit coverage (manifest-flag authority, native post-filtering for tables/functions/cross-refs, and SQLState/errorCode assertions). Verified the DatabricksSQLException constructor overload used in DatabricksThriftAccessor (now preserves the source errorCode — an improvement), the getTablesResult signature migration across all callers, and null-safety of the new matchesParent/copyThriftNativeMetadataRows paths; no correctness issues found. One low-severity note on an inconsistent SQLState class for null- vs empty-table validation. Separately, per CLAUDE.md's telemetry-error guidance, the reclassification of key-based validation errors from INVALID_STATE to EXECUTE_STATEMENT_FAILED should have the driver/server/user classification recorded in the maintainers' telemetry taxonomy — worth confirming that was linked in the PR.

throw new DatabricksSQLException(
"Invalid argument: tableName may not be null or empty",
DatabricksDriverErrorCode.INVALID_STATE);
OPERATION_ERROR_SQLSTATE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — For the same validation message ("Invalid argument: tableName may not be null or empty"), a null table throws with SQLState 42000 (syntax/access rule violation) while an empty table throws with SQLState 08000 (OPERATION_ERROR_SQLSTATE). SQLState class 08 denotes connection exception, which is semantically wrong for an argument-validation failure — a caller branching on the SQLState class would treat an empty-string table name as a connection problem. This also diverges from listExportedKeys (line 419), where a null table correctly uses 42000. The two null/empty branches for identical error text emitting different SQLState classes reads as an oversight rather than intent.

If this deliberately mirrors what the Thrift server returns for empty identifiers, a brief code comment would prevent it from looking like a copy/paste divergence. Otherwise consider using 42000 for both.

@vuanhphung
vuanhphung added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit a9a0f17 Aug 26, 2026
27 checks passed
@vuanhphung
vuanhphung deleted the vu-phung_data/productionize-thrift-native-metadata branch August 26, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants